(function ($) {

  var H5Utils = {
    captureObj: false,
    captureResponsor: {},
    isChrome: false,
    isChromeLegacy: false,
    isFirefox: false,
    isEdge: false,
    i18n: {
      zh_CN: {
        findStaff: {
          placeholder: '输入账号、工号、登录名或者姓名',
          tip: '输入模糊查询字符串后，系统将自动匹配，并提供选择。只有在选择后才可提交确认。<br/>选项格式：[工号:账号-登录名] 姓名',
          dialogTip: '选择账号',
          assertFailed: '获取账号联想失败: '
        }NO
      },
      en_US: {
        findStaff: {
          placeholder: 'Username/AgentId',
          tip: 'Choose data item after associate.<br/>Format：[AgentId:StaffId-Loginname] Fullname',
          dialogTip: 'Find staff',
          assertFailed: 'Associate failed: '
        }
      }
    },

    initCodeMirror: function (callback) {
      require(['js/lib/codemirror/lib/codemirror',
          'js/lib/codemirror/addon/hint/show-hint',
          'js/lib/codemirror/addon/hint/elite-hint',
          'js/lib/codemirror/mode/javascript/javascript',
          'js/lib/codemirror/mode/css/css',
          'js/lib/codemirror/mode/sql/sql',
          'js/lib/codemirror/mode/sql/jsonsql',
          'js/lib/codemirror/mode/xml/xml',
          'js/lib/codemirror/mode/properties/properties'],
        function (codemirror) {
          window.CodeMirror = codemirror;

          Helper.loadCSS($E.url + '/js/lib/codemirror/lib/codemirror.css', 'codemirrorStyles');
          Helper.loadCSS($E.url + '/js/lib/codemirror/theme/elite-ve.css', 'codemirrorStyles2');
          callback && callback()
        })
    },

    initCkEditor: function (callback) {
      require(['js/ckeditor/ckeditor'], function () {
        CKEDITOR.disableAutoInline = true;
        if ($.isFunction(callback)) {
          try {
            callback();
          } catch (e) {
            console.error(e)
          }
        }
      })
    },

    htmlEditor: function (options, callback) {
      if ($.type(options) == 'function') {
        callback = options;
        options = {};
      }
      options = $.extend({
        width: '80vw',
        height: '80vh',
        title: 'HTML编辑器',
        okTip: '确定并复制到剪贴板',
        closeOnEscape: true,
        content: '',
        fade: 0,
        config: 'config.js?v=' + $E.ver
      }, options)
      var ta = $('<textarea id="redactor_htmlEditor_TA" style="width:100%;height:100%"></textarea>'),
        busyId = $E.progressing || $E.silent ? '' : $F.busyShow(),
        editor;
      this.initCkEditor(function () {
        $F.createDialog(ta, {
          title: options.title,
          escapable: options.closeOnEscape,
          okTip: options.okTip,
          removable: true,
          contentCSS: {
            'padding': '0px',
            'overflow': 'hidden',
            'width': options.width,
            'height': options.height
          },
          onOpen: function () {
            $('.dialogContent', this).resizable();
            CKEDITOR.config.uploadUrl = $E.url + '/fs/uploadFile';
            CKEDITOR.config.filebrowserImageUploadUrl = $E.url + '/fs/uploadImg';
            CKEDITOR.config.imageUploadUrl = $E.url + '/fs/uploadPDImg';
            CKEDITOR.config.captureUploadUrl = $E.url + '/fs/uploadCapture';
//						CKEDITOR.config.disallowedContent = 'script; *[on*]';

            editor = CKEDITOR.replace(ta[0], {
              customConfig: options.config,
              on: {
                instanceReady: function (ev) {
                  this.container.addClass('elite');
                  this.container.$.tabIndex = 0;
                  this.on('blur', function () {
                    this.container.removeClass('focus');
                    if (this.checkDirty()) {
                      // console.info(this)
                      ta.val(this.getData()).trigger('change')
                    }
                  });
                  this.on('focus', function () {
                    this.container.addClass('focus');
                  });
                  this.setData(options.content);
                  $F.busyHide(busyId);
                }
              }
            })
          },
          onSubmit: function (close) {
            var code = editor.getData();
            close();
            if ($.isFunction(callback))
              callback(code);
          }
        })
      })
    },

    getFontIcon: function (type, callback, current, keep) {
      var doc;
      if (type === 0) {
        doc = $('#fontAwesomeLib.as');
      } else if (type === 1) {
        doc = $('#fontAwesomeLib.es');
      } else if (type === 3) {
        doc = $('#fontAwesomeLib.sp');
      }
      var cache = [],
        selected,
        busyId = $E.projects || $E.silent ? '' : $F.busyShow(),
        iconClick = function (e, doc) {
          var e = $(e.currentTarget);
          selected = e.attr('data-ref');
          $('.headbox .tag', doc).text(selected);
          $('a.fonticon', doc).removeClass('selected');
          e.addClass('selected');
          // 如果选中行不在显示区内，则滚动
          var top = e.position().top, ul = $('ul', doc);
          if (top + e.outerHeight() > ul[0].clientHeight) {
            ul.scrollTop(top + e.outerHeight() - ul[0].clientHeight + ul.scrollTop());
          } else if (top < 0)
            ul.scrollTop(top);

          $('a.ok', doc).show();
        };

      if (doc.length == 0) {
        var fontTypeUrl;
        if (type === 0) {
          fontTypeUrl = '/res/font-as.html?v=';
        } else if (type === 1) {
          fontTypeUrl = '/res/font-es.html?v=';
        } else if (type === 3) {
          fontTypeUrl = '/res/font-sp.html?v=';
        }
        $.get($E.url + fontTypeUrl + $E.ver).done(function (doc) {
          doc = $(doc);
          $('body').append(doc);
          var search = $('input', doc).on('keydown', function (e) {
            if (e.keyCode == 13)
              searchCallback();
          });
          Helper.makeHelpInput(search, '[s]times-circle', function () {
            search.val('');
            searchCallback();
          })

          var icons = $('a.fonticon', doc);
          icons.each(function (i, v) {
            var this$ = $(v);
            cache.push({
              dom: this$.parent(),
              tag: this$.attr('data-ref')
            })
          })
          icons.on('click', function (e) {
            e.preventDefault();
            iconClick(e, doc);
          });
          $('.result .count', doc).text(cache.length);
          var searchCallback = function () {
            var e = search.val();
            //console.log('Search font icon: ' + e);
            $F.await(function (go) {
              if (e) {
                var r = 0;
                $.each(cache, function (i, v) {
                  if (eval('/' + e + '/gi').test(v.tag)) {
                    r++;
                    v.dom.show();
                  } else
                    v.dom.hide();
                })
                $('.fa_result .count', doc).text(r);
              } else {
                $('li', doc).show();
                $('.fa_result .count', doc).text(cache.length);
              }
              go();
            });
          }
          $('a.cancel', doc).on('click', function (e) {
            e.preventDefault();
            doc.fadeOut();
          })
          $('a.ok', doc).on('click', function (e) {
            e.preventDefault();
            if (!keep)
              doc.fadeOut();
            if (selected)
              callback(selected);
          })
          $('a.fonticon[data-ref="' + current + '"]', doc).click();
          search.focus();
        }).always(function () {
          $F.busyHide(busyId)
        })
      } else {
        setTimeout(function () {
          $('a.ok', doc).hide();
          $('.tag', doc).text('');
          $('input', doc).val('');
          var icons = $('a.fonticon', doc).show();
          icons.each(function (i, v) {
            var this$ = $(v);
            cache.push({
              dom: this$,
              tag: this$.attr('data-ref')
            })
          })
          icons.off('click').on('click', function (e) {
            e.preventDefault();
            iconClick(e, doc);
          });
          $('.result .count', doc).text(icons.length);
          $('a.ok', doc).off('click').on('click', function (e) {
            e.preventDefault();
            if (!keep)
              doc.fadeOut();
            if (selected)
              callback(selected);
          })
          doc.show();
          $('a.fonticon[data-ref="' + current + '"]', doc).click();
          $('input', doc).focus();
          $F.busyHide(busyId)
        }, 1)
      }
    },

    swapList: function (listA, listB, compare) {
      var entryA = listA.getData(),
        entryB = listB.getData(),
        data;
      $.each(listA.getSelectedDom(), function (i, li) {
        data = li.data('_entity');
        entryA.remove(data);
        entryB.push(data);
        li.remove();
      })
      if ($.isFunction(compare))
        entryB.sort(compare);
      listB.refresh();
    },
    swapListAll: function (listA, listB, compare) {
      var entryA = listA.getData(),
        entryB = listB.getData();
      $.each(entryA, function (i, data) {
        entryB.push(data);
      })
      if ($.isFunction(compare))
        entryB.sort(compare);
      listB.refresh();
      entryA.length = 0;
      listA.refresh();
    },

    /**
     * 将内容复制到系统剪贴板中
     * @param data {any} 复制到系统剪贴板中的内容
     * @param callback {function} 复制完成后的回调方法
     * @param silent {boolean} 是否使用安静模式复制（复制完成后不提示信息）
     */
    clip: function (data, callback, silent) {
      var copyContent = getStringData(data);
      if (copyContent === '' || copyContent === undefined)
        $F.notify('复制失败，不能对空内容进行复制', -1);
      else {
        //console.log(copyContent);
        if (window.clipboardData) {
          // browser support clipboardData
          window.clipboardData.setData('Text', copyContent);
          if ($.isFunction(callback))
            callback(copyContent);
          // else
          // 	$F.notify('已将内容复制到剪贴板')
        } else {
          if (document.queryCommandSupported('copy')) {
            var ta = $('#_clipCopyTA');
            if (ta.length == 0)
              ta = $('<textarea id="_clipCopyTA" />').appendTo($('body'));
            try {
              ta.val(copyContent).select().focus();
              if (document.execCommand('copy', false, '')) {
                if ($.isFunction(callback))
                  callback(copyContent);
                else {
                  // if (! silent)
                  // 	$F.notify('已将内容复制到剪贴板')
                }
              } else {
                if (!silent)
                  $F.alert('复制到剪贴板失败')
              }
            } catch (err) {
              $F.err('H5Utils - copy', err);
            }
          } else
            $F.alert('浏览器不支持CLIPBOARD操作');
        }
      }
    },

    storage: function (name, data, callback) {
      if (!window.localStorage)
        return;

      try {
        window.localStorage.setItem(name, data);
        callback && callback();
      } catch (e) {
      }
    },

    /**
     * 在插件中打开百度地图定位地址
     * baiduMap(address)
     * 在独立的弹出窗口中打开百度地图定位地址
     * baiduMapAlone(address)
     * @param address {string}  地址字符串
     *
     * 在插件中打开百度地图定位坐标
     * baiduMap(latitude, longitude, label, marks)
     * 在独立的窗口中打开百度地图定位坐标
     * baiduMapAlone(latitude, longitude, label, marks)
     * @param latitude {float}  经度
     * @param longitude {float} 纬度
     * @param [label] {string}  标签
     * @param [marks] {array}  标记
     */
    baiduMap: function (latitude, longitude, label, marks) {
      if (!latitude) {
        $F.err('CmdUtils - baiduMap', '不合法的百度地图函数入参');
        return;
      }

      var project = $E.getActiveProject(), parameter;
      if (!longitude) {
        // 按照地址入参
        parameter = 'location=' + latitude;
      } else {
        // 按照地理坐标入参
        parameter = 'latitude=' + latitude + '&longitude=' + longitude + (label == undefined ? '' : ('&label=' + label)) + (marks == undefined ? '' : ('&markers=' + marks));
      }
      $E.openPlugin('地图', project, 'baiduMap', '', parameter);
    },

    baiduMapAlone: function (latitude, longitude, label, marks) {
      if (!latitude) {
        $F.err('CmdUtils - baiduMap', '不合法的百度地图函数入参');
        return;
      }
      var parameter;
      if (!longitude) {
        // 按照地址入参
        parameter = 'location=' + latitude;
      } else {
        // 按照地理坐标入参
        parameter = 'latitude=' + latitude + '&longitude=' + longitude + (label == undefined ? '' : ('&label=' + label)) + (marks == undefined ? '' : ('&markers=' + marks));
      }
      var url = $E.url + '/baiduMaps.html?v=0.1&' + parameter;
      window.open(url, "_blank", 'alwaysRaised=yes, top=0,left=0, fullscreen=yes, location=no, directories=no, toolbar=no, status=no, menubar=no, scrollbars=no, resizable=yes');
    },

    /**
     * 窗口右下角的浮动层消息提示
     * @method popup
     * @param options {object} 消息提示参数
     *  options.title {string} 标题
     *  options.message {string} 正文
     *  [options.event] {string} 点击正文触发的event
     *  [options.eventData] {object} event数据
     *  [options.urgency] {boolean} 是否紧急
     *  [options.skin] {string} 皮肤
     *  [options.type] {string} warn info error 图标
     *  [options.button] {boolean} 按钮
     *  [options.certain] {boolean} 确定按钮
     *  [options.cancel] {boolean} 取消按钮
     *  [options.lastTime] {int} 持续时间(毫秒)
     */
    popup: function (options) {
      if (!options.message)
        return;

      let dlg = $('<div class="elitePopup"><div class="title abbr">' +
          '<i class="fa fa-info-circle"></i><span>POPUP</span>' +
          '</div>' +
          '<a href="#" class="js_close fa fa-times"></a>' +
          '<div class="inner"></div></div>'),
        head = $('.title', dlg),
        inner = $('.inner', dlg);

      $('#elitePopupContainer').append(dlg);
      $('.js_close', dlg).off('click').on('click', function (e) {
        e.preventDefault();
        dlg.removeData('animation')
        dlg.remove();
      })
      if (options.skin) {
        dlg.addClass(options.skin)
      }
      let i = $('i', head), s = $('span', head);
      if (!options.urgency) {
        i.attr('class', 'blue fa fa-info-circle');
        inner.removeClass('red')
      } else {
        i.attr('class', 'red fa fa-exclamation-circle');
        inner.addClass('red');
      }
      if (options.type === 'warn') {
        i.attr('class', 'warn fa fa-info-circle'); //消息提示
      } else if (options.type === 'info') {
        i.attr('class', 'info fa fa-info-circle'); //操作失败
      } else if (options.type === 'error') {
        i.attr('class', 'error fa fa-info-circle'); //错误提示
      }
      s.html(options.title || '消息');
      let expired = false, msgbox = $('<div class="js_msg abbr">' + options.message + '</div>').appendTo(inner);
      if (options.type) {
        msgbox.addClass('msgbox_margin')
      }
      if (options.callback || options.event) {
        msgbox.css('cursor', 'pointer').click(function (e) {
          if (expired)
            return;

          expired = true;
          removeMsg(dlg, $(e.currentTarget));
          if (options.callback)
            options.callback(options.eventData);
          else
            $E.getActiveProject().events.notify(options.event, options.eventData);
        })
      }

      if (options.button) {
        let buttons = $('<div class="eliteBtn" />').appendTo(inner);
        if (options.cancel) {
          let cancel_btn = $('<div class="js_cancel">' + '取消' + '</div>').appendTo(buttons)
          cancel_btn.off('click').on('click', function (e) {
            e.preventDefault();
            dlg.removeData('animation')
            dlg.remove();
          })
        }
        if (options.certain) {
          let certain_btn = $('<div class="js_certain">' + '确定' + '</div>').appendTo(buttons)
          certain_btn.off('click').on('click', function (e) {
            e.preventDefault();
            dlg.removeData('animation')
            dlg.remove();
          })
        }
      }

      let lastTime = options.lastTime || 10000;
      if (dlg.data('animation')) {
        setTimeout(function () {
          removeMsg(dlg, msgbox);
        }, lastTime);
      } else {
        dlg.data('animation', true);
        dlg.show('clip', {}, 500, function () {
          setTimeout(function () {
            removeMsg(dlg, msgbox);
          }, lastTime);
        });
      }
    },

    /**
     * 打开一个快速输入框
     * @param options {object} 输入框参数
     *  options.title {string} 输入框标题
     *  options.type {string} 输入框类型：text, textarea
     *  options.pattern {string} 输入结果校验正则格式
     *  options.placeholder {string} 输入框占位提示
     *  options.value {string} 输入框默认值
     *  options.escapable {boolean} 是否允许escape按键取消
     *  options.clickToHide {boolean} 是否允许其他区域点击鼠标取消
     *  options.required {boolean} 是否必填
     *  options.width {integer} 输入框宽度
     *  options.height {integer} 输入框高度（仅textarea模式有效）
     *  options.onSubmit {function} 输入完毕后的回调方法
     */
    input: function (opts) {
      opts = $.extend({
        title: '',
        type: 'text',
        pattern: '',
        placeholder: '',
        value: '',
        escapable: true,
        clickToHide: false,
        required: true,
        width: 0, // auto
        height: 0, // auto
        onSubmit: function (t) {
          return true;
        }
      }, opts);

      var dialog = $('<div id="h5input" tabIndex=-1><form /></div>').appendTo($('body')),
        form = $('form', dialog),
        input;
      if (opts.title) {
        form.append($('<p class="title" />').text(opts.title))
      }
      var errormsg = $('<p class="error red none" />').appendTo(form);
      if (opts.type == 'textarea') {
        input = $('<textarea spellcheck="false" placeholder="' + opts.placeholder + '" />').appendTo(form);
      } else {
        input = $('<input type="text" spellcheck="false" placeholder="' + opts.placeholder + '" />').appendTo(form).on('keydown', function (e) {
          if (e.keyCode == 13)
            onSubmit();
        })
        if (opts.pattern)
          input.attr('pattern', opts.pattern)
      }
      input.val(opts.value || '');
      if (opts.type == 'number')
        Helper.makeNumberInput(input);

      input.prop('required', opts.required ? true : false);
      if (opts.width || opts.inputWidth)
        input.css('width', opts.width || opts.inputWidth);
      if (opts.height || opts.inputHeight)
        input.css('height', opts.height || opts.inputHeight);

      var btns = $('<div class="btns"></div>').appendTo(form),
        okBtn = $('<button type="submit" class="flat_green" />')
          .text($F.lang('message.ok'))
          .appendTo(btns),
        onSubmit = function () {
          errormsg.empty().hide();
          if (opts.onSubmit(input.val(), function (error) {
            if (error)
              errormsg.append(error).show();
          })) {
            Helper.masker.hide(dialog);
          } else
            input.select().focus();
        };
      if (!opts.clickToHide) {
        $('<button type="button" class="flat_red" />')
          .text($F.lang('message.cancel'))
          .appendTo(btns)
          .on('click', function () {
            Helper.masker.hide(dialog);
          })
      }
      if (opts.escapable) {
        dialog.off('keydown').on('keydown', function (e) {
          if (e.keyCode == 27) {
            Helper.masker.hide(dialog);
          }
        })
      }

      form.on('submit', function (e) {
        e.preventDefault();
        onSubmit();
      })
      Helper.masker.show(dialog, {
        escapeable: true,
        clickToHide: opts.clickToHide,
        onOpen: function () {
          setTimeout(function () {
            input.focus();
          }, 10)
        },
        onClose: function () {
          dialog.remove();
        }
      })
    },

    /**
     * 在DOM侧边显示快速输入框
     * @method popupInput
     * @param {dom} dom 在指定的dom上进行浮动展示，通常可以使用click事件中的R.CTRL.dom
     * @param {object} [options] 可选的创建参数. 可用的属性包括
     *    [options.title] {string} 标题信息
     *    [options.type] {string} 输入类型,包括text,textarea，默认为text
     *    [options.placeholder] {string} 输入提示
     *    [options.value] {any} 输入的默认值
     *    [options.width] {integer} 输入框宽度，单位为像素
     *    [options.alignToInput] {string} 输入框与目标dom的对齐模式，包括"start", "center"和"end"三种模式
     *    [options.onSubmit] {function} onSubmit: function(value), 返回true时关闭输入框
     * @example
     *  // event on control click
     *  H5Utils.popupInput(R.CTRL.dom, {
     * 		title: '请输入证件号码',
     * 		placeholder: '15位数字与阿拉伯数字',
     * 		value: '310103199012120918',
     * 		width: 300,
     * 		alignToInput: 'start',
     * 		onSubmit: function(value) {
     * 			if (Verify.isIdentify(value)) {	// 验证身份证格式是否合法
     * 				cmdAsync('doMyCmd', {		// 异步调用命令doMyCmd
     * 					ID: value				// 将身份证信息作为ID变量传入命令
     * 				})
     * 				return true;				// 返回true，允许输入框关闭
     * 			}
     * 		}
     * 	})
     */
    popupInput: function (dom, opts) {
      opts = $.extend({
        title: 'Input: ',
        type: 'text',
        pattern: '',
        placeholder: '',
        value: '',
        width: 200, // auto,
        alignToInput: 'center',
        onSubmit: function (t) {
          return true;
        }
      }, opts);

      var input;
      if (opts.type == 'text') {
        input = $('<input type="text" style="width:100%;margin-top:10px" class="js_input" spellcheck="false" pattern="' + opts.pattern + '" placeholder="' + opts.placeholder + '" />')
          .on('keydown', function (e) {
            if (e.keyCode == 13)
              okBtn.trigger('click');
          });
      } else if (opts.type == 'textarea') {
        input = $('<textarea style="width:100%;margin-top:10px" class="js_input" spellcheck="false" placeholder="' + opts.placeholder + '" />');
      } else {
        alert("Cann't identify popup form type.");
        return;
      }
      if (opts.maxLength)
        input.attr('maxLength', opts.maxLength);

      input.css({
        'box-sizing': 'border-size',
        width: parseInt(opts.width) + 'px'
      })

      var container = $('<div style="display:none;word-wrap: break-word;word-break: break-all;padding: 15px 20px">'
        + '<div class="js_popupInput_content">'
        + '<h4 class="js_popupInput_title" style="font-size:14px;font-weight:700;color:gray"></h4>'
        + '</div><div class="js_popupInput_bar" style="text-align:right;margin-top:15px;">'
        + '<button type="button" class="js_submit_btn flat_blue" style="padding:4px 2em; min-width:60px;">' + $F.lang('message.ok') + '</button>'
        + '</div></div>');
      $('h4.js_popupInput_title', container).text(opts.title || '');
      $('.js_popupInput_content', container).append(input);
      var okBtn = $('button.js_submit_btn', container);

      Helper.popover($(dom), container, {
        stopClickPropagation: true,
        alignToInput: opts.alignToInput,
        blockSelfClick: true,
        onOpen: function (closeFunc) {
          input.val(opts.value);
          input.select().focus();

          okBtn.on('click', function (e) {
            if (opts.onSubmit(input.val())) {
              closeFunc();
            } else
              input.select().focus();
          })
        }
      })
    },

    getFile: function (filePath, saveas) {
      let url = $E.url + '/fs/get?file=' + encodeURI(filePath);
      if (saveas)
        url += '&d=true';
      window.open(url, '_elite_file_download');
    },

    /**
     * 下载录音
     * @method getRecord
     * @param id {string} 数据库记录中的录音文件ID
     * @param [type] {integer} 录音文件ID对应的TYPE，默认是record_guid
     */
    getRecord: function (id, type) {
      var url = $E.vrsUrl + '/GetRecord?id=' + id + '&queryType=' + type + '&type=download';
      window.open(url);
    },
    /**
     * 批量下载录音
     * @method getRecord
     * @param id {string} 数据库记录中的录音文件ID，多个用,分隔
     * @param [queryType] {String}查询类型
     */
    getRecordBatch: function (id, queryType) {
      var url = $E.vrsUrl + '/GetBatchRecord';

      if ($("#getRecordBatch_iframe_temp")) {
        $("#getRecordBatch_iframe_temp").remove();
      }
      if ($("#getRecordBatch_form_temp")) {
        $("#getRecordBatch_form_temp").remove();
      }

      var iframe = $("<iframe>");
      iframe.attr("type", "hidden");
      iframe.attr("src", "about:blank");
      iframe.attr("name", "getRecordBatch");
      iframe.attr("style", "width:0; height:0; margin-top:-10px");
      iframe.attr("id", "getRecordBatch_iframe_temp");
      $("body").append(iframe);

      var form = $("<form>");
      form.attr("style", "display:none");
      form.attr("target", "getRecordBatch");
      form.attr("method", "post");
      form.attr("action", url);
      form.attr("id", "getRecordBatch_form_temp");
      var input1 = $("<input>");
      input1.attr("type", "hidden");
      input1.attr("name", "id");
      input1.attr("value", id);

      var input2 = $("<input>");
      input2.attr("type", "hidden");
      input2.attr("name", "queryType");
      input2.attr("value", queryType);

      $("body").append(form);
      form.append(input1);
      form.append(input2);
      form.submit();
    },

    /**
     * 将某个输入控件变为颜色选择器
     */
    colorPicker: function (input, callback) {
      input = $(input);
      require(['js/lib/colorpicker/color.all.min', 'js/lib/colorpicker/jqColor' + $E.jsv], function () {
        input.colorPicker({
          customBG: '#222',
          noAlpha: true,
          init: function (elm, colors) {
            // colors is a different instance (not connected to colorPicker)
            input.css({
              background: elm.value || '#FFFFFF',
              color: (colors.rgbaMixCustom.luminance > 0.22 ? '#222222' : '#dddddd')
            });
          }
        })
        if ($.isFunction(callback))
          callback();
      })
    },

    /**
     * 在指定位置展现浮动层
     */
    drift: function (top, left, bottom, right, panel, options) {
      var parent = panel.parent(),
        parentOffset = parent.offset(),
        parentScrollLeft = parent.scrollLeft(),
        parentScrollTop = parent.scrollTop(),
        x = left - parentOffset.left - parentScrollLeft,
        y = bottom - parentOffset.top - parentScrollTop + 2;

      //console.log('top:' + y + ', left:' + x);
      if (panel.height() == 0)
        panel.show();

      if (y + panel.outerHeight() - parentScrollTop > parent[0].offsetHeight) {
        var ny = top - panel.outerHeight() - parentOffset.top - parentScrollTop - 1;
        if (ny > 0)
          y = ny;
      }
      if (x + panel.outerWidth() - parentScrollLeft > parent[0].offsetWidth) {
        var nx = right - panel.outerWidth() - parentOffset.left - parentScrollLeft;
        if (nx > 0)
          x = nx;
      }
      panel.css({
        position: 'absolute',
        top: y + 'px',
        left: x + 'px',
        zIndex: 1,
        display: 'none'
      })
      if (options && options.zIndex)
        panel.css('zIndex', options.zIndex);

      Helper.slide(panel, {
        complete: (options && options.callback)
      });
    },

    capture: function (mode, callback, uploadUrl) {
      if (!$.isFunction(callback))
        return $F.err('未设定截图回调方法'), false;

      this.captureWithoutWS(new Date().getTime() + '.png', mode, callback, uploadUrl, true);
    },

    captureWithoutWS: function (tmpFileName, mode, callback, uploadUrl, autoUpload) {
      var a = this;
      if (!uploadUrl)
        uploadUrl = $E.url + '/fs/uploadCapture';

      var doCapture = function () {
        var captureRet;
        a.tmpFileName = tmpFileName;
        switch (mode) {
          case 1:
            // 不隐藏当前窗口
            captureRet = a.captureObj.DoCapture(tmpFileName, false, 0, 0, 0, 0, 0);
            break;
          case 2:
            // 预选窗口
            captureRet = a.captureObj.DoCapture(tmpFileName, false, 3, 0, 0, 0, 0);
            break;
          case 3:
            // 截屏
            captureRet = a.captureObj.DoCapture(tmpFileName, false, 2, 0, 0, 0, 0);
            break;
          default:
            // 隐藏当前窗口
            captureRet = a.captureObj.DoCapture(tmpFileName, true, 0, 0, 0, 0, 0);
        }

        if (captureRet == emCaptureFailed) {
          showDownload();
        } else if (captureRet == emCaptureUnknown) {
          showDownload();
        }
      };

      var downloadUrl = 'http://www.ggniu.cn/download/CaptureInstall.exe?' + new Date().getTime();
      var showDownload = function () {
        $F.alert(i18n.capture.installAlert1 + '<a style="text-decoration: underline;color: blue;" target="_blank" href="' + downloadUrl + '">' + i18n.capture.installAlert2 + '</a><br>' + i18n.capture.win10Shortcut, null, true);
      };

      if (!a.captureObj) {
        a.captureObj = new NiuniuCaptureObject();
        a.captureObj.NiuniuAuthKey = "niuniu";
        // 此处可以设置相关参数
        a.captureObj.TrackColor = rgb2value(255, 0, 0);
        a.captureObj.EditBorderColor = rgb2value(0, 0, 255);
        a.captureObj.VersionCallback = function (version) {
          $F.log('H5Utils - capture', 'Capture version: ' + version);
        };
        a.captureObj.PluginLoadedCallback = function () {
          $F.log('H5Utils - capture', 'Capture plugin loaded.');
        };

        // 初始化控件
        a.captureObj.InitNiuniuCapture();
        if ($E.debug)
          console.log('NiuniuCapture - initialized');

        // 设置控件加载完成以及截图完成的回调函数
        a.captureObj.FinishedCallback = function (type, x, y, width, height, info, content, localpath) {
          if (type < 0) {
            // 需要重新安装控件
            showDownload();
            return;
          }
          switch (type) {
            case 1:
              if ($E.debug)
                console.log('截图完成： x:' + x + ',y:' + y + ',width:' + width + ',height:' + height);
              if (autoUpload) {
                uploadDataURL(content, localpath, uploadUrl, callback);
              } else {
                callback({
                  code: 1,
                  content: content,
                  tmpFileName: a.tmpFileName
                });
              }
              break;
            case 2:
              if ($E.debug)
                console.log('取消了截图');
              callback({
                code: -1,
                message: $F.lang('capture.canceled')
              });
              break;
            case 3:
              if ($E.debug)
                console.log('保存了截图到本地： x:' + x + ',y:' + y + ',width:' + width + ',height:' + height);
              if (autoUpload) {
                uploadDataURL(content, localpath, uploadUrl, callback);
              } else {
                callback({
                  code: 1,
                  content: content,
                  tmpFileName: a.tmpFileName
                });
              }
              break;
            case 4:
              if (info == '0') {
                if ($E.debug)
                  console.log('从剪贴板获取到了截图： ' + localpath);
                if (autoUpload) {
                  uploadDataURL(content, localpath, uploadUrl, callback);
                } else {
                  callback({
                    code: 1,
                    content: content,
                    tmpFileName: a.tmpFileName
                  });
                }
              } else {
                if ($E.debug)
                  console.log('从剪贴板获取图片失败');
                callback({
                  code: -1,
                  message: '从剪贴板获取图片失败'
                });
              }
              break;
          }
        };


        setTimeout(function () {
          doCapture();
        }, 500);
      } else {
        doCapture();
      }
    },


    /**
     * 选择本地图片文件进行裁切后上传服务器
     * @param options {object}
     *  [options.fsKey] {string} 文件服务器key
     *  [options.fsPath] {string} 上传文件服务器的目录
     *  [options.aspectRatio] {float} 图片文件裁切长宽比
     * @param callback {function} 上传完毕后回调方法，
     * @example
     *  H5Utils.cutImg({
     * 		fsPath: '/myCutImages',
     * 		aspectRatio: 1
     * 	}, function(result) {
     * 		console.log(result)
     * 	})
     */
    cutImg: function (options, callback) {
      options = $.extend({
        fsKey: '',
        fsPath: '/cutImages',
        oldPhoto: '',
        masker: $E.url + '/images/default_image.png',
        aspectRatio: 1.5
      }, options);

      var dom = $('<div class="cutImageDialog">'
        + '<div class="org_photo"><img class="js_photo" src="" alt="[原图]"></div>'
        + '<div class="preview-pane"><div class="preview-pane-box"><div class="preview-container">'
        + '<img tabIndex=0 src="" class="jcrop-preview" alt="[预览]"></div></div>'
        + '<input type="text" name="cropWidth" placeholder="缩放宽度" disabled="disabled">'
        + '<input type="text" name="cropHeight" placeholder="缩放高度" disabled="disabled">'
        + '</div><textarea style="width:0;height:0;display:none"></textarea><input type="file" accept="image/*" style="display:none;"></div>');
      var img = $('.js_photo', dom).show().css({
        width: 'auto',
        height: 'auto'
      });

      var $preview = $('.preview-pane', dom),
        $pcnt = $('.preview-container', $preview),
        $pimg = $('img', $pcnt).attr('src', ''),
        imgFileData;
      if (options.aspectRatio) {
        $pcnt.css({
          height: (150 / options.aspectRatio) + 'px'
        })
      }

      var renderImageData = function () {
        var reader = new FileReader();
        reader.onload = function () {
          renderImage(reader.result);
        }
        reader.readAsDataURL(imgFileData);
        $('.btnsBar .js_submit', dialog).removeData('coords');
      }, renderImage = function (src) {
        var tm = new Image();
        tm.src = src;
        tm.onload = function () {
          img.attr('src', src).css({
            width: 'auto',
            height: 'auto'
          });
          $pimg.attr('src', src).css({
            width: '100%',
            height: '100%',
            margin: 0
          });
          cropper()
        }
        //img[0].src = $pimg[0].src = reader.result;
        //cropper()
      }

      var dialog;
      $F.createDialog(dom, {
        icon: '[d]image',
        title: '图像裁切',
        removable: true,
        okTip: '提交裁切',
        contentCSS: {
          'padding': '0',
          'overflow': 'auto'
        },
        onOpen: function () {
          dialog = this;
          $('.btnsBar .js_submit', dialog).addClass('flat_green').prop('disabled', true);
          img[0].src = $pimg[0].src = options.masker;
          $('<button type="button" class="flat" style="margin-right:8px">打开图片</button>')
            .prependTo($('.btnsBar', dialog))
            .on('click', function (e) {
              $('input[type=file]', dialog).on('change', function (e) {
                imgFileData = this.files[0];
                if (imgFileData.type.match(/image.*/)) {
                  renderImageData();
                }
              }).val('').click()
            });
          $(document.body).on('paste', function (e) {
            var clipboardData = e.originalEvent.clipboardData || window.clipboardData,
              items = clipboardData.items,
              types = clipboardData.types || [];

            // 保存在剪贴板中的数据类型
            var item = items[0];
            for (var i = 0; i < types.length; i++) {
              if (types[i] === 'Files') {
                item = items[i];
                break;
              }
            }
            // 判断是否为图片数据
            // console.log(item);
            if (item && item.kind === 'file' && item.type.match(/^image\//i)) {
              imgFileData = item.getAsFile();
              renderImageData();
            }
          })

          if (options.oldPhoto) {
            img.attr('alt', '图片载入中...')
              .attr('src', $E.url + '/images/loading.gif')
              .css({
                width: 'auto',
                height: 'auto'
              });
            var url = $E.url + '/fs/get?file=' + options.oldPhoto;
            if (options.oldPhoto.toLowerCase().startsWith('res:'))
              url = $E.url + '/' + srcPath.substring(4);
            renderImage(url)
          }

          Helper.makeNumberInput($('input[name=cropWidth]', dom))
          Helper.makeNumberInput($('input[name=cropHeight]', dom))

          if (!options.aspectRatio) {
            let ratio = $('<div class="extra" style="line-height:2.3em"><span style="margin-right:10px">裁切比：</span>'
              + '<label style="margin-right:10px"><input type="radio" name="ratio" value="0" checked>自由</label>'
              + '<label style="margin-right:10px"><input type="radio" name="ratio" value="1">1:1</label>'
              + '<label style="margin-right:10px"><input type="radio" name="ratio" value="1.5">3:2</label>'
              + '<label style="margin-right:10px"><input type="radio" name="ratio" value="2">2:1</label></div>');
            ratio.appendTo($('.btnsBar', dialog));
            $('input[name=ratio]', ratio).on('click', function () {
              let value = parseFloat($('input[name=ratio]:checked', ratio).val())
              if (value) {
                $pcnt.css('height', (150 / value) + 'px')
              } else {
                $pcnt.css('height', '')
              }
              options.aspectRatio = value;
              cropper()
            })
          }
        },
        onSubmit: function (close) {
          var coords = $('.btnsBar .js_submit', dialog).data('coords'),
            cw = $('input[name=cropWidth]', dom).val() || 0,
            ch = $('input[name=cropHeight]', dom).val() || 0;
          if (!imgFileData) {
            // using oldPhoto
            if (options.oldPhoto.toLowerCase().startsWith('res:'))
              $F.notify('资源文件不支持裁切', -1);
            else {
              var busyId = $F.busyShow();
              Ajax.post({
                url: $E.url + '/fs/cutImage2_',
                headers: {
                  token: $E.getActiveProject().token
                },
                data: {
                  fsKey: options.fsKey,
                  fsPath: options.fsPath,
                  original: options.oldPhoto,
                  x: Math.round(coords.x),
                  y: Math.round(coords.y),
                  w: Math.round(coords.w),
                  h: Math.round(coords.h),
                  iw: Math.round(img.width()),
                  cw: parseInt(cw),
                  ch: parseInt(ch)
                },
                success: function (data) {
                  close();
                  setTimeout(function () {
                    callback(data);
                  }, 1)
                },
                always: function () {
                  $F.busyHide(busyId)
                }
              })
            }
            return;
          }

          const form = $('<form />')
            .attr('action', $E.url + '/fs/cutImage_?ulpid=' + $E.uid)
            .attr('data-ulpid', $E.uid)
            .appendTo($('body'));
          coords.iw = Math.round(img.width());
          $('<input type="hidden" name="euKey" />').appendTo(form).val(options.fsKey),
            $('<input type="hidden" name="euPath" />').appendTo(form).val(options.fsPath),
            $('<input type="hidden" name="euName" />').appendTo(form).val(JSON.stringify({
              x: Math.round(coords.x),
              y: Math.round(coords.y),
              w: Math.round(coords.w),
              h: Math.round(coords.h),
              iw: Math.round(img.width()),
              cw: parseInt(cw),
              ch: parseInt(ch)
            }));

          var result = form.upload({
            type: 'POST',
            statusUrl: $E.url + '/upload/status?ulpid=' + $E.uid,
            fileData: imgFileData,
            success: function (data) {
              close();
              setTimeout(function () {
                callback(data);
              }, 1)
            },
            error: function (e) {
              if (e.status == 500)
                $F.notify('Upload faild: ' + e.status, -1);
              else
                $F.notify('[' + e.status + ']' + e.statusText, -1);
            },
            always: function () {
              form.remove();
            }
          })
        },
        onClose: function () {
          $(document.body).off('paste');
        }
      })

      var cropper = function () {
        if ($E.jcropApi) {
          $E.jcropApi.release();
          $E.jcropApi.destroy();
          delete $E.jcropApi;
        }
        $('.jcrop-holder', dialog).remove();

        var boundx,
          boundy,
          xsize = $pcnt.width(),
          ysize = $pcnt.height();

        function updatePreview(c) {
          console.log(c);
          if (parseInt(c.w) > 0) {
            var rx = xsize / c.w;
            var ry = ysize / c.h;
            $('input[name=cropWidth]', dom).val(c.w);
            $('input[name=cropHeight]', dom).val(c.h);
            // console.log(`rx: ${rx} ry: ${ry}  xsize: ${xsize}  ysize: ${ysize}  boundx: ${boundx} boundy: ${boundy}`);
            $pimg.css({
              width: Math.round(rx * boundx) + 'px',
              height: Math.round(ry * boundy) + 'px',
              marginLeft: '-' + Math.round(rx * c.x) + 'px',
              marginTop: '-' + Math.round(ry * c.y) + 'px'
            });
            $('.btnsBar .js_submit', dialog).prop('disabled', false).data('coords', c);
          }
        }

        img.Jcrop({
          onChange: updatePreview,
          // onSelect : updatePreview,
          aspectRatio: options.aspectRatio
        }, function (api) {
          var bounds = api.getBounds();
          boundx = bounds[0];
          boundy = bounds[1];
          $E.jcropApi = api;
          $('input[name=cropWidth]', dom).val(boundx);
          $('input[name=cropHeight]', dom).val(boundx);
          //api.animateTo([0, 0, img.width(), img.height()])
          updatePreview({
            x: 0,
            y: 0,
            w: Math.round(img.width()),
            h: Math.round(img.height())
          })
        })
      }
    },

    /**
     * 用于手机中的图片裁剪
     */
    cropper: function () {
      return {
        /**
         * 开始裁剪
         * @method doCrop
         * @param img {obj|string} 图片jquery对象 或者 图片对象选择器字符串
         * @param callback {function} 裁剪回调函数
         */
        doCrop: function (img, callback) {
          $(img).cropper({
            aspectRatio: 1 / 1, //裁剪框比例1:1
            viewMode: 1,
            guides: false,
            crop: function (e) {
              if ($.isFunction(callback)) {
                callback(e);
              }
            }
          });
        },
        /**
         * 上传裁剪的图片
         * @method uploadCrop
         * @param img {obj|string} 图片jquery对象 或者 图片对象选择器字符串
         * @param callback {function} 上传完回调函数
         */
        uploadCrop: function (img, uploadPath, callback) {
          var uploadeUrl = $E.url + '/fs/upload_',
            imgSrc = $(img).attr('src'),
            imgType = imgSrc.substring(imgSrc.lastIndexOf('.') + 1) || 'jpeg';

          var busyId = $F.busyShow('Image uploading...');
          $(img).cropper('getCroppedCanvas').toBlob(function (blob) {
            var formData = new FormData();
            formData.append('euFile', blob);
            formData.append('euKey', '');
            formData.append('euPath', uploadPath);
            formData.append('euName', $E.staff.id + '-' + $F.randomString(16) + '.' + imgType);

            $.ajax(uploadeUrl, {
              method: "POST",
              headers: {
                token: $E.getActiveProject().token
              },
              data: formData,
              processData: false,
              contentType: false
            }).done(function (result) {
              $F.log('H5Utils - uploadCrop', 'UploadCrop Done, download path:' + result.value, result);
              var ev = {
                RESULT: {
                  CODE: result.code,
                  MESSAGE: result.message || '',
                  VALUE: result.value || ''
                }
              };
              callback(ev);
            }).fail(function (jqXHR, textStatus, errorThrown) {
              $F.err('H5Utils - uploadCrop', 'UploadCrop failed: ' + JSON.stringify(errorThrown));
            }).always(function () {
              $F.busyHide(busyId);
            });
          }, 'image/' + imgType);
        }
      }
    },

    loadImage: function (imgDom, source) {
      imgDom.attr('src', $E.url + '/images/loading.gif');
      var image = new Image();
      image.onload = function (e) {
        imgDom.attr('src', e.path[0].currentSrc);
      }
      image.src = source;
    },

    /**
     * 打开一个模拟窗口
     * var win = $F.window({ width: 600, height: 400, title: '新窗口', model: true});
     *
     * @method window
     * @param [options] {object} 窗口属性参数
     *  [options.title] {string} 窗口标题
     *  [options.width] {integer} 窗口宽度
     *  [options.height] {integer} 窗口高度
     *  [options.left] {integer} 窗口定位left坐标
     *  [options.top] {integer} 窗口定位top坐标
     *  [options.model] {boolean} 是否模态窗口
     * @return {dom} 窗口的DOM
     */
    window: function (options) {
//			options = $.extend(options, {
//				id : $F.randomString(8),
//				title : i18n.message.newwindow,
//				width : 300,
//				height : 200,
//				model : true,
//				minable : true,
//				maxable : true,
//				closable : true,
//				beforeClose : false,
//				afterClose : false
//			}, options);

      var minwin = findMinimalWindow(options.id);
      if (minwin) {
        var dialog = minwin.data('window');
        minwin.click();
        return dialog;
      }
      var dialog = $('#' + options.id + '_window');
      if (dialog.length > 0)
        return dialog;

      dialog = $("<div id='" + options.id + "_window' class='fancybox dyn-window' />");
      dialog.data('options', options);
      var fancybox_body = $("<div class='fancybox-body' />").appendTo(dialog);
      if (!$F.isNull(options.title)) {
        var title = $("<div class='fancybox-title'>" + options.title + "</div>").appendTo(fancybox_body);
      }
      var btnBar = $("<div class='fancybox-btns' />").appendTo(fancybox_body);
      var closeBtn = $("<a title='" + $F.lang('message.close') + "' class='fancybox-close' href='javascript:;'></a>").appendTo(btnBar).click(function (e) {
        // console.log(options);
        if ($.isFunction(options.beforeClose)) {
          if (!options.beforeClose())
            return;
        }
        if (options.model)
          Helper.masker.hide(dialog);
        else
          dialog.hide();
        dialog.remove();
        if ($.isFunction(options.afterClose)) {
          options.afterClose();
        }
      });
      var maxBtn = $("<a title='" + $F.lang('message.maximal') + "' class='fancybox-max' href='javascript:;'></a>").appendTo(btnBar).click(function (e) {
        if (maxBtn.attr("data-fullscreen")) {
          maxBtn.removeAttr("data-fullscreen");
          maxBtn.attr('title', $F.lang('message.maximal'));
          var left = maxBtn.attr("data-left"),
            top = maxBtn.attr("data-top"),
            cwidth = maxBtn.attr("data-cwidth"),
            cheight = maxBtn.attr("data-cheight"),
            cmaxwidth = maxBtn.attr("data-cmaxwidth"),
            cmaxheight = maxBtn.attr("data-cmaxheight");
          dialog.removeClass("fullscreen").css({
            width: 'auto',
            height: 'auto',
            left: left,
            top: top
          });
          content.css({
            width: cwidth,
            height: cheight,
            maxWidth: cmaxwidth,
            maxHeight: cmaxheight
          })
        } else {
          maxBtn.attr("data-fullscreen", "fullscreen");
          maxBtn.attr('title', $F.lang('message.restore'));
          maxBtn.attr("data-left", dialog.css('left'));
          maxBtn.attr("data-top", dialog.css('top'));
          maxBtn.attr("data-cwidth", content.css('width'));
          maxBtn.attr("data-cheight", content.css('height'));
          maxBtn.attr("data-cmaxwidth", content.css('maxWidth'));
          maxBtn.attr("data-cmaxheight", content.css('maxHeight'));
          dialog.addClass('fullscreen').css({
            width: '100%',
            height: '100%',
            left: 0,
            top: 0
          });
          content.css({
            width: 'auto',
            height: 'auto',
            maxHeight: 'none',
            maxWidth: 'none'
          })
        }
      });
      var minBtn = $("<a title='最小化' class='fancybox-min' href='javascript:;'></a>").appendTo(btnBar).click(function (e) {
        if (options.model)
          Helper.masker.hide(dialog);
        else
          dialog.hide();
        var winfolder = $("footer .f_wins");
        var win = $("<a href='#' class='f_mini_win' data-id='" + options.id + "' title='" + title.text() + "' />")
          .click(function (e) {
            e.preventDefault();
            if (options.model)
              Helper.masker.show(dialog);
            else
              dialog.show();
            win.remove();
          })
          .data("window", dialog)
          .appendTo(winfolder);
        win.tooltip();
      });
      if (!options.closable)
        closeBtn.hide();
      if (!options.maxable)
        maxBtn.hide();
      if (!options.minable)
        minBtn.hide();
      var content = $("<div class='fancybox-content' />")
        .appendTo(fancybox_body)
        .css({
          width: parseInt(options.width) + 'px',
          height: parseInt(options.height) + 'px'
        })
      var container = $("<div class='fancybox-container' />")
        .appendTo(content)
        .css({
          'max-height': (window.innerHeight - 50) + 'px',
          'max-width': (window.innerWidth - 20) + 'px'
        })
      if (options.body) {
        container.append(options.body);
      }
      dialog.appendTo($('body')).show();
      //console.log(dialog.outerWidth());
      //console.log(dialog.outerHeight());
      if (options.model) {
        Helper.masker.show(dialog);
      } else {
        dialog.show();
        var left = ($(window).width() - dialog.outerWidth()) / 2,
          top = ($(window).height() - dialog.outerHeight()) / 2;
        dialog.css({
          "left": (left < 0 ? 0 : left + "px"),
          "top": (top < 0 ? 0 : top + "px"),
          "zIndex": 10003
        })
      }

      dialog.draggable({
        // handle : title,
        // containment : "window",
        scroll: false,
        drag: function (event, ui) {
          ui.position.left = Math.max(0, ui.position.left);
          ui.position.top = Math.max(0, ui.position.top);
          ui.position.left = Math.min(window.innerWidth - 100, ui.position.left);
          ui.position.top = Math.min(window.innerHeight - 30, ui.position.top);
        }
      });
      if (options.left > -1)
        dialog.css({
          left: options.left + 'px'
        })
      if (options.top > -1)
        dialog.css({
          top: options.top + 'px'
        })

      return dialog;
    },

    dynSelector: function (options) {
      require.config({
        urlArgs: 'v=' + Math.random()
      });
      require(['js/commons/dynSelector' + $E.jsv], function (selector) {
        selector(options)
      });
    },

    dyn2Selector: function (options) {
      require.config({
        urlArgs: 'v=' + Math.random()
      });
      require(['js/commons/dyn2Selector' + $E.jsv], function (selector) {
        selector(options)
      });
    },

    woSelector: function (options) {
      require.config({
        urlArgs: 'v=' + Math.random()
      });
      require(['js/commons/woSelector' + $E.jsv], function (selector) {
        selector(options)
      });
    },

    wo3Selector: function (options) {
      require.config({
        urlArgs: 'v=' + Math.random()
      });
      require(['js/commons/wo3Selector' + $E.jsv], function (selector) {
        selector(options)
      });
    },

    objSelector: function (options) {
      require.config({
        urlArgs: 'v=' + Math.random()
      });
      require(['js/commons/objSelector' + $E.jsv], function (selector) {
        selector(options)
      });
    },

    rolegroupSelector: function (options) {
      require.config({
        urlArgs: 'v=' + Math.random()
      });
      require(['js/commons/rolegroupSelector' + $E.jsv], function (selector) {
        selector(options)
      });
    },

    rolegroupTypeSelector: function (options) {
      require.config({
        urlArgs: 'v=' + Math.random()
      });
      require(['js/commons/rolegroupTypeSelector' + $E.jsv], function (selector) {
        selector(options)
      });
    },

    /**
     *  options: {
     * 		token: {string},
     * 		epidSql: {string},
     * 		current: {string|array},
     * 		multi: {boolean}
     *	}
     */
    staffSelector: function (options) {
      require.config({
        urlArgs: 'v=' + Math.random()
      });
      require(['js/commons/staffSelector' + $E.jsv], function (selector) {
        selector(options)
      });
    },

    findStaff: function (options) {
      let lang = H5Utils.i18n[$E.lang] || H5Utils.i18n['zh_CN'],
        box = $('<div />'),
        input = $('<input type="text" style="width:350px" />')
          .prop('placeholder', lang.findStaff.placeholder)
          .on('keydown', function (e) {
            if (input.data('staff')) {
              input.removeData('staff');
              input.val('');
            }
            $('button.js_submit', dialog).prop('disabled', true);
          }).appendTo(box);
      box.append($('<div style="margin:10px 0;max-width:350px;color:#666"><i>' + lang.findStaff.tip + '</i></div>'));

      let dialog = $F.createDialog(box, {
        title: lang.findStaff.dialogTitle,
        container: options.container,
        zIndex: options.zIndex,
        contentCSS: {
          'padding': '30px',
          'overflow': 'hidden'
        },
        onOpen: function () {
          let dialog = this;
          $('button.js_submit', dialog).prop('disabled', true);
          input.autocomplete({
            source: function (request, response) {
              let str = ('%' + request.term + '%').toUpperCase(), sqlKey;
              if (!options.filter)
                sqlKey = 'System.Staff.find';
              else {
                if (options.scope == 'role')
                  sqlKey = 'System.Staff.find.withRole';
                else
                  sqlKey = 'System.Staff.find.withGroup';
              }
              $F.dataService('SystemDo2', {
                data: $F.sqlbean(sqlKey, options.token, [str, options.filter], 20),
                digest: options.digest,
                success: function (data) {
                  if (data.code < 1)
                    return $F.err('H5Utils - findStaff', lang.findStaff.assertFailed + data.message);

                  //console.log(data);
                  if (data.value[0].count == -1) {
                    $F.err('H5Utils - findStaff', lang.findStaff.assertFailed + data.value[0].key);
                  } else {
                    let ar = [];
                    $.each(data.value[0].fields, function (i, v) {
                      ar.push({
                        label: '[' + v[0] + ': ' + v[1] + ' - ' + v[6] + '] ' + v[2],
                        category: v
                      });
                    })
                    response(ar);
                  }
                }
              });
            },
            minLength: 2,
            select: function (event, ui) {
              if (ui.item) {
                // console.log(ui.item);
                input.val(ui.item.label).select().focus().data('staff', {
                  id: ui.item.category[1],
                  name: ui.item.category[2],
                  agentId: ui.item.category[0],
                  supervisor: ui.item.category[3],
                  rolegroup: ui.item.category[4],
                  headImg: ui.item.category[5]
                });
                $('button.js_submit', dialog).prop('disabled', false);
              }
            },
            open: function () {
              $(this).removeClass("ui-corner-all").addClass("ui-corner-top");
            },
            close: function () {
              $(this).removeClass("ui-corner-top").addClass("ui-corner-all");
            }
          }).focus();
        },
        onSubmit: function (close) {
          var staff = input.data('staff');
          close();
          if ($.isFunction(options.callback))
            options.callback(staff);
        },
        onCancel: function (close) {
          close.call();
        }
      })
    },

    roleSelector: function (options) {
      require.config({
        urlArgs: 'v=' + Math.random()
      });
      require(['js/commons/roleSelector' + $E.jsv], function (selector) {
        selector(options)
      });
    },

    doctemplateSelector: function (options) {
      require.config({
        urlArgs: 'v=' + Math.random()
      });
      require(['js/commons/doctemplateSelector' + $E.jsv], function (selector) {
        selector(options)
      });
    },

    questSelector: function (options) {
      require.config({
        urlArgs: 'v=' + Math.random()
      });
      require(['js/commons/questSelector' + $E.jsv], function (selector) {
        selector(options)
      });
    },

    webquestSelector: function (options) {
      require.config({
        urlArgs: 'v=' + Math.random()
      });
      require(['js/commons/webquestSelector' + $E.jsv], function (selector) {
        selector(options)
      });
    },

    browseFolder: function (msg, callback) {
      if ($F.localProxyReady()) {
        $E.ws.local.send("util", {
          action: "browseFolder",
          msg: msg
        }, callback);
      } else {
        $F.log('h5utils - browseFolder', 'localproxy is not ready.');
      }
    },

    /**
     * 在有localproxy，可以通过此方法去打开一个ie窗口
     * @param url
     */
    openIE: function (url) {
      if ($F.localProxyReady()) {
        $E.ws.local.send("util", {
          action: "openIE",
          url: url
        });
      } else {
        $F.log('h5utils - openIE', 'localproxy is not ready.');
      }
    },

    /**
     * 在有localproxy的时候，可以通过此方法去post提交一个表单到新打开的一个IE窗口
     * @param url
     * @param data 格式为一个数组，没个数组元素需要有name和value属性
     * [
     *  {
     * 		name: 'a1', value: 'bbb'
     * 	},
     *  {
     * 		name: 'a2', value: 'ccc'
     * 	}
     * ]
     * @param name 表示打开窗口名字，需要自己指定一个名字
     */
    openIEPost: function (url, data, name) {
      if ($F.localProxyReady()) {
        $E.ws.local.send("util", {
          action: "openIEPost",
          url: url,
          data: data,
          name: name || 'IEPOST'
        });
      } else {
        $F.log('h5utils - openIEPost', 'localproxy is not ready.');
      }
    },

    /**
     * 在有localproxy，可以通过此方法发出一个本地的ajax，实现ajax的跨域请求
     * @param param 具体ajax的参数，url必须要有，默认method为POST
     * {
     * 		url : 'http://xxxxxx/asdfa',
     * 		method : 'POST',
     * 		headers : '{"header1" : "value1", "header2" : "value2"}'
     * 		body : "xxxx"
     * }
     * @param callback 回调函数
     * {
     * 		status : 200, //200表示成功，0表示异常，http的标准response status
     * 		responseBody : '' // 具体返回的内容字符串
     * }
     *
     * 示例：H5Utils.localAJAX({url:'http://127.0.0.1:8980/ngs/js/login.js',method:'GET', headers: '{"Content-Type":"application/x-www-form-urlencoded"}', body:''}, function(d){console.log(d)})
     */
    localAJAX: function (param, callback) {
      if ($F.localProxyReady()) {
        if (param && param.url) {
          param.method = param.method || 'POST';
          param.headers = param.headers || '{"Content-Type":"application/x-www-form-urlencoded"}';
          param.body = param.body || '';
          var data = {
            action: 'ajax',
            param: param
          };
          $F.log('h5utils - localAJAX', 'Call param:', param);
          $E.ws.local.send('util', data, function (d) {
            $F.log('h5utils - localAJAX', 'Call result:', d);
            if ($.isFunction(callback)) {
              callback(d);
            }
          }, {});
        } else {
          $F.log('h5utils - localAJAX', 'No url specified.');
        }
      } else {
        $F.log('h5utils - localAJAX', 'localproxy is not ready.');
      }

    },

    /**
     * 设置默认打印机
     * @param name 打印机名
     * @param callback 回调
     */
    setDefaultPrinter: function (name, callback) {
      if ($F.localProxyReady()) {
        $E.ws.local.send('util', {
          action: "uin",
          param: {
            name: name
          }
        }, function (d) {
          if ($.isFunction(callback)) {
            callback(d);
          }
        }, {});
      } else {
        $F.log('h5utils - setDefaultPrinter', 'localproxy is not ready.');
      }
    },

    /**
     * 获取本机信息（IP MAC 机器名）
     * @param callback 回调
     */
    getLocalInfo: function (callback) {
      if ($F.localProxyReady()) {
        $E.ws.local.send('util', {
          action: "getLocalInfo"
        }, function (d) {
          if ($.isFunction(callback)) {
            callback(d);
          }
        }, {});
      } else {
        $F.log('h5utils - getLocalInfo', 'localproxy is not ready.');
      }
    },

    /**
     * 打开mediaPlayer，并播放指定的url的音乐文件
     * @param url 音乐文件url
     */
    openMediaPlayer: function (url) {
      if ($F.localProxyReady()) {
        $E.ws.local.send("util", {
          action: "openMediaPlayer",
          url: url
        });
      } else {
        $F.log('h5utils - openMediaPlayer', 'localproxy is not ready.');
      }
    },

    /**
     * 通过localproxy，直接执行一段shell脚本
     * @param shell 脚本
     */
    runShell: function (shell) {
      if ($F.localProxyReady()) {
        $E.ws.local.send("util", {
          action: "runShell",
          shell: shell
        });
      } else {
        $F.log('h5utils - runShell', 'localproxy is not ready.');
      }
    },

    encodeRFC5987ValueChars: function (str) {
      return encodeURIComponent(str).
        // Note that although RFC3986 reserves "!", RFC5987 does not,
        // so we do not need to escape it
        replace(/['()]/g, escape). // i.e., %27 %28 %29
        replace(/\*/g, '%2A').
        // The following are not required for percent-encoding per RFC5987,
        // so we can allow for a little better readability over the wire: |`^
        replace(/%(?:7C|60|5E)/g, unescape);
    },


    /**
     * @method notifyMe  在支持Notification的浏览器环境中，可以通过此方法发出提醒
     * @param {string} [title] 弹出提示的title
     * @param {string} [body] 弹出提示的body
     * @param {string} [icon] 弹出提示的图标
     * @param {string} [tag] 弹出标签，同样的tag会让后一个notification覆盖前一个
     * @param {function} [clickCallback] 点击弹出提示后回调
     */
    notifyMe: function (body, title, icon, tag, clickCallback) {
      if (!$E.WebNotification) {
        return;
      }
      if ($E.WebNotification.permission !== "granted")
        $E.WebNotification.requestPermission();
      else {
        var opts = {
          dir: "auto",
          lang: "hi",
          requireInteraction: false,
          tag: "chat",
          renotify: true,
          silent: false,
          noscreen: false,
          sticky: true,
          icon: icon || 'css/themes/greenSkin/logo.png',
          body: body || '您有新的消息'
        };
        if (tag) {
          opts.tag = tag;
        }
        var notification = new $E.WebNotification(title || '新消息', opts);
        notification.onclick = function () {
          if ($.isFunction(clickCallback)) { //自定义点击行为
            clickCallback();
          } else { //默认点击行为，打开窗口
            // chrome only
            // If the window is minimized, restore the size of the window
            window.open().close();
            window.focus();
          }
          notification.close();
        };
      }
    },

    loadPlugin: function (options) {
      options = $.extend({
        async: true,
        done: function (dom) {
          $F.log('Function - loadPlugin', "Plugin " + options.name + " load completely");
        }
      }, options);
      if (!options.container)
        return $F.err('Function - loadPlugin', '未指定插件容器');

      var prePath = $E.url + '/js/plugins/';
      if (options.css !== undefined) {
        if ($.type(options.css) === 'array')
          $.each(options.css, function (i, css) {
            Helper.loadCSS(prePath + css, 'plugin_loadCSS_' + options.name + '_' + i);
          });
        else
          Helper.loadCSS(prePath + options.css, 'plugin_loadCSS_' + options.name);
      }

      if (options.html) {
        var busyId = $F.busyShow(), url = $E.url + '/plugins/' + options.html;
        return $.ajax({
          cache: false,
          url: url,
          async: options.async
        }).done(function (html) {
          var dom = $(html);
          options.container.append(dom);
          if ($.isFunction(options.done))
            options.done(dom)
        }).fail(function () {
          $F.alert("不能读取插件的HTML脚本: " + url);
        }).always(function () {
          $F.busyHide(busyId);
        });
      } else {
        options.done();
      }
    },

    /**
     *  根据状态码，解析电话号码状态
     * @param state
     * @returns {*}
     */
    parseNumberState: function (state) {
      var UNCHECKED = "0";
      var CHECKED = "1";
      var WRONGNUMBER = "2";
      if (state == WRONGNUMBER) {
        return $F.lang('outbound.numberState.wrongNumber');
      } else if (state == CHECKED) {
        return $F.lang('outbound.numberState.checked');
      } else {
        return $F.lang('outbound.numberState.unchecked');
      }
    },

    widgetsPerfer: function (widgets, callback) {
      var dom = $('<div style="display:flex;flex-flow:column;width:600px;height:400px;">'
        + '<p style="padding:4px 10px;margin-bottom:6px;border:1px solid #EFF181;background:#FFFFE7"><i class="far fa-info-circle"></i> 点击序号拖动行，或者按CTRL+↑↓ 键操作行移动</p>'
        + '<div style="flex:1"><table></table></div>'
        + '</div>');

      var table = $('table', dom).wisTable({
        cookie: 'widgetsPerfer.table',
        hideColumnMenu: true,
        movable: true,
        draggable: true,
        tabIndex: -1,
        columns: [{
          display: $('<input type="checkbox" class="js_all_ckb" />').on('click', function (e) {
            var this$ = $(e.currentTarget), state = this$.prop('checked');
            $('.js_item_ckb', dom).each(function (i, ckb) {
              ckb = $(ckb);
              if (ckb.prop('disabled'))
                return;
              ckb.prop('checked', state);
              ckb.data('entity').state = state ? 1 : 0;
            })
          }),
          width: 60,
          align: 'center'
        }, {
          display: '字段名',
          width: 200
        }, {
          display: '描述',
          width: 310
        }],
        models: function (data, row, col, tr) {
          switch (col) {
            case 0:
              var input = $('<input type="checkbox" class="js_item_ckb" />').prop('checked', data.state ? true : false);
              if (data.require)
                input.prop('checked', true).prop('disabled', true);
              else
                input.on('click', function (e) {
                  var this$ = $(e.currentTarget), state = this$.prop('checked');
                  this$.data('entity').state = state ? 1 : 0;
                  if (!state)
                    $('.js_all_ckb', dom).prop('checked', false);
                }).data('entity', data);
              return input;
            case 1:
              if (data.require)
                return '<span class="red">* ' + data.name + '</span>';
              else
                return data.name;
            case 2:
              return data.desc || '';
          }
        }
      }).setData(widgets || []);

      $F.createDialog(dom, {
        removable: true,
        icon: 'adjust',
        title: '控件显隐选择器',
        onOpen: function () {

        },
        onSubmit: function (close) {
          var cs = table.getData();
          close();
          callback(cs);
        },
        onCancel: function (close) {
          close();
        }
      })
    },

    fullscreen: function (dom) {
      if (document.mozFullScreenEnabled || document.fullscreenEnabled || document.webkitFullscreenEnabled || document.msFullscreenEnabled) {
        // further code goes here
        var ev = function (e) {
          var dom = $(e.currentTarget);
          if (dom.hasClass('fullscreen')) {
            dom.css({
              position: dom.attr("data-normal-position"),
              width: dom.attr("data-normal-width"),
              height: dom.attr("data-normal-height")
            }).trigger('resize');
            dom.removeClass('fullscreen');
          } else {
            dom.css({
              position: 'static',
              width: '100%',
              height: '100%'
            });
            setTimeout(function () {
              dom.trigger('resize');
            }, 100);
            dom.addClass('fullscreen');
          }
        }
        dom.off('webkitfullscreenchange', ev).on('webkitfullscreenchange', ev);
        var ele = dom[0];
        // going full-screen
        if (ele.requestFullscreen) {
          ele.requestFullscreen();
        } else if (ele.webkitRequestFullscreen) {
          ele.webkitRequestFullscreen();
        } else if (ele.msRequestFullscreen) {
          ele.msRequestFullscreen();
        } else if (ele.mozRequestFullScreen) {
          ele.mozRequestFullScreen();
        }
        dom.attr("data-normal-width", dom.css('width'));
        dom.attr("data-normal-height", dom.css('height'));
        dom.attr("data-normal-position", dom.css('position'));
      } else {
        console.error('浏览器不支持全屏模式');
      }
    },

    /**
     * 上传文件到指定的目录
     *
     *  Sample: 上传图片文件到服务器resources/temp目录，图片尺寸不能大于2M
     *  H5Utils.fileUpload({
     * 		filepath: 'resources/temp',
     * 		allowFiles: ['png','gif'],
     * 		maxSize: 2048,
     * 		callback: function(result, ev){
     * 		},
     * 		ev: evObject
     * 	});
     *
     * @method fileUpload
     * @param [options] {object} 入参对象
     *      [fsKey]: {string}, 文件存储服务名，为空时取默认服务
     *      [filePath]: {string}, 文件存储相对路径，为空时存储于temp临时目录，作为临时文件存在
     *      [fileName]: {string}, 文件存储名，相同时覆盖。为空时由系统自动生成存储名
     *      [allowFiles]: {string | array} 允许上传的文件类型后缀名，多个文件类型时，使用array：['pnp', 'jpg', ...]
     *      [maxSize]: {integer} 允许上传的文件最大尺寸，单位为KB
     *      [maxTotalSize]: {integer} 当同时上传多个文件时，允许上传的所有文件总和的最大尺寸，单位为KB
     *      [timeToLive]: {integer} 文件保存时效
     *      [isTemp]: {boolean} 是否临时文件
     *      [callback]: {string | function} 文件上传完毕时执行的回调命令或方法，执行结果为：R.RESULT{CODE, MESSAGE, VALUE}
     *      [options.ev] {object} 执行回调的环境入参
     *      [multipart] {boolean} 是否允许多文件同时上传 默认为否
     *      [statusUrl] {string} 进度获取接口地址
     *  callback : function(result, ev) {
     * 		// result: 标准json执行结果对象
     * 		// ev: 入参环境
     * 	}
     */
    fileUpload: function (options) {
      options = $.extend({
        allowFiles: [],
        maxSize: 0,
        maxTotalSize: 0,
        callback: function (e) {
        },
        statusUrl: $E.url + '/fs/upload/status?ulpid=' + $E.uid,
      }, options);
      for (let i = 0; i < options.allowFiles.length; i++) {
        options.allowFiles[i] = options.allowFiles[i].toUpperCase();
      }
      let form = $('#upload-form');
      if (form.length === 0) {
        form = $('<form id="upload-form" style="display: none"/>')
          .attr('data-ulpid', $E.uid)
          .appendTo($('body'));
        $('<input type="hidden" name="euPath" />').appendTo(form).val(options.filePath);
        $('<input type="hidden" name="euName" />').appendTo(form).val(options.fileName);
        $('<input type="hidden" name="euKey" />').appendTo(form).val(options.fsKey);
        $('<input type="hidden" name="euTemp" />').appendTo(form).val(options.isTemp ? 'true' : 'false');
        $('<input type="hidden" name="multipart" />').appendTo(form).val(options.multipart ? 'true' : 'false');
        $('<input type="hidden" name="meta" />').appendTo(form).val(options.meta ? 'true' : 'false');
        $('<input type="hidden" name="showOriginalName" />').appendTo(form).val(options.showOriginalName ? 'true' : 'false');
      } else {
        form.resetForm();
        $('input[name=euPath]', form).val(options.filePath);
        $('input[name=euName]', form).val(options.fileName);
        $('input[name=euKey]', form).val(options.fsKey);
        $('input[name=euTemp]', form).val(options.isTemp ? 'true' : 'false');
        $('input[name=multipart]', form).val(options.multipart ? 'true' : 'false');
        $('input[name=meta]', form).val(options.meta ? 'true' : 'false');
        $('input[name=showOriginalName]', form).val(options.showOriginalName ? 'true' : 'false');
      }
      if (options.preview)
        form.attr('action', $E.url + '/fs/importAndPreview_?ulpid=' + $E.uid);
      else
        form.attr('action', $E.url + '/fs/upload_?ulpid=' + $E.uid);

      if (options.fileData) {
        const result = form.upload({
          type: 'POST',
          statusUrl: options.statusUrl,
          maxSize: options.maxSize,
          allows: options.allowFiles,
          fileData: options.fileData,
          success: function (data) {
            if (data.code < 0)
              $F.notify(data.message, data.code);
            else
              options.callback(data, options.ev);
          },
          error: function (e) {
            if (e.status == 500) {
              options.callback({
                code: -9,
                message: 'Upload failed: ' + e.status
              }, options.ev);
            } else {
              options.callback({
                code: -1,
                message: '[' + e.status + ']' + e.statusText
              }, options.ev);
            }
          },
          always: function () {
            form.remove();
          }
        });
        if (result.code < 0)
          $F.notify(result.message, -1);
      } else {
        let euFile;
        if ($('input[name=euFile]', form).length === 0) {
          euFile = $('<input type="file" name="euFile" />').appendTo(form);
        } else {
          euFile = $('input[name=euFile]');
        }
        form.attr('enctype', options.multipart ? 'multipart/form-data' : '');
        form.find('input[name=euFile]').attr('multiple', options.multipart ? 'multiple' : 'false');
        euFile.attr('accept', options.accept ? options.accept : '');

        euFile.off('change').on('change', function () {
          // 当不知道上传文件名具体值，但又不想上传的文件名后面拼接上时间戳时,
          // 可以通过参数showOriginalName来决定上传的文件名是否用该原始文件的文件名
          // options.multipart为true时无效
          if (options.showOriginalName && !options.multipart)
            form.find('input[name=euName]').val(this.files[0].name.split('.')[0]);
          const result = form.upload({
            type: 'POST',
            statusUrl: options.statusUrl,
            maxSize: options.maxSize,
            maxTotalSize: options.maxTotalSize,
            allows: options.allowFiles,
            success: function (data) {
              if (data.code < 0)
                $F.notify(data.message, data.code);
              else
                options.callback(data, options.ev);
            },
            error: function (e) {
              if (e.status == 500) {
                options.callback({
                  code: -9,
                  message: 'Upload failed: ' + e.status
                }, options.ev);
              } else {
                if (e.status === 413) {
                  options.callback({
                    code: -1,
                    message: '文件尺寸超过服务器或者网管的限制'
                  }, options.ev);
                } else if (e.status === 0 && e.readyState === 0) {
                  options.callback({
                    code: -2,
                    message: '文件上传被取消'
                  }, options.ev);
                } else {
                  options.callback({
                    code: -1,
                    message: '[' + e.status + ']' + e.statusText
                  }, options.ev);
                }
              }
            },
            always: function () {
              form.remove();
            }
          });
          if (result.code < 0)
            $F.notify(result.message, -1);
        }).click();
      }
      return true;
    },

    /**
     * http文件下载
     * @method fileDownload
     * @param url {string} 文件路径
     */
    fileDownload: function (url) {
      window.open(url);
    },

    /**
     * 删除资源文件
     * @param filePath {string} 资源文件在服务器上的存储路径
     * @param [callback] {callback} 执行完毕的回调函数
     */
    fileRemove: function (filePath, callback) {
      $.ajax({
        type: 'post',
        url: $E.url + 'fs/remove_',
        data: {
          f: filePath
        }
      }).done(function (data) {
        if ($.isFunction(callback))
          callback(data)
      });
    },

    domEncode: function (htmlStr) {
      var reg = /^<[^>]+^>/g;
      if (!reg.test(htmlStr))
        htmlStr = '<span>' + htmlStr + '</span>';

      try {
        $(htmlStr);
      } catch (e) {
        htmlStr = '<span>' + htmlStr + '</span>';
      }
      return {
        html: htmlStr
      }
    },

    domDecode: function (cell) {
      if ($.type(cell) == 'object')
        return (cell && cell.html) ? cell.html : JSON.stringify(cell);

      return cell;
    },

    /**
     * 打开一个模态的进度条
     * @method progress
     * @param options {object} 进度条属性参数
     *  [options.title] {string} 进度条标题
     *  [options.width] {integer} 进度条宽度
     *  [options.showValue] {boolean} 是否显示进度条数值
     * @return {object} 进度条组件
     *
     *  进度条组件对象的属性及方法：
     *  var progress = $F.progress({width: 300});
     *  progress.value({integer});  // 设置该进度条的进度值，于0至100间
     *  progress.finish();  // 完成进度条。进度条窗体自动销毁
     *  progress.reset();  // 重值进度条
     *
     */
    progress: function (options) {
      var a = new Object();
      a.options = $.extend({
        width: 300,
        showValue: false
      }, options);
      a.prog = $('<div class="elite_progress"></div>').appendTo('body');
      // append title dom
      a.ep_title = $('<div class="ep_title">当前进度：</div>').appendTo(a.prog);
      // append progress dom
      a.ep_progress = $('<div class="ep_progress_bar" style="position:relative" />').appendTo(a.prog);
      a.ep_label = $('<div class="ep_progress_label wrapper" style="text-align:center;line-height:2em"></div>').appendTo(a.ep_progress);
      // set title for progress
      if (a.options.title)
        a.ep_title.text(a.options.title);
      a.ep_progress.css({
        width: a.options.width + 'px'
      });
      a.ep_progress.progressbar({
        value: false,
        change: function () {
          if (options.showValue)
            a.ep_label.text(a.ep_progress.progressbar("value").toFixed(0) + "%");
        }
      })

      a.title = function (str) {
        this.ep_title.text(str);
      }
      a.finish = function () {
        Helper.masker.hide(this.prog);
        this.prog.remove();
        $E.progressing = false;
      }
      a.reset = function () {
        this.value(0);
      }
      a.value = function (e) {
        this.ep_progress.progressbar("option", {
          value: e
        })
      }
      Helper.masker.show(a.prog);
      $E.progressing = true;

      if (options.process) {
        setTimeout(function () {
          options.process.call(a);
        }, 10)
      }
      return a;
    },

    openLocalFile: function (callback) {
      var input = $('#_h5utils_openlocalfile');
      if (!input.length) {
        input = $('<input type="file" id="_h5utils_openlocalfile" style="display:none" />').appendTo($('body'));
      }
      input.off('change').on('change', function (e) {
        var file = this.files[0];
        //console.log('导入文件类型：' + file.type);
        var reader = new FileReader();
        reader.readAsText(file);
        reader.onload = function (e) {
          callback(file, this.result);
        }
        this.value = '';
      }).trigger('click');
    },

    VoiceCapture: {
      init: function (callback) {
        if ($F.localProxyReady()) {
          $E.ws.local.send('voicecapture', {
            action: "init"
          });
          $E.ws.local.hook($CONST.EVENT.WS_VOICECAPTURE, function (data, ev) {
            if (callback) {
              callback(data, ev);
            }
          }, {}, true);
          return true;
        } else {
          $F.log('h5utils - VoiceCapture.init', 'LocalProxy is not ready.');
          return false;
        }
      },
      start: function () {
        if ($F.localProxyReady()) {
          $E.ws.local.send('voicecapture', {
            action: "start"
          });
        } else {
          $F.log('h5utils - VoiceCapture.start', 'LocalProxy is not ready.');
        }
      },
      stop: function (callback) {
        if ($F.localProxyReady()) {
          $E.ws.local.send('voicecapture', {
            action: "stop"
          });
        } else {
          $F.log('h5utils - VoiceCapture.stop', 'LocalProxy is not ready.');
        }
      },
      destroy: function (callback) {
        if ($F.localProxyReady()) {
          $E.ws.local.send('voicecapture', {
            action: "destroy"
          });
        } else {
          $F.log('h5utils - VoiceCapture.destroy', 'LocalProxy is not ready.');
        }
      }
    },

    /**
     * 开始录屏
     * @param bizId {string} 业务流水号
     * @param description {string} 录屏业务描述
     * @param [options] {object} 入参对象
     * @param [options.packSize] {number} 每集合多少个event事件之后提交一次服务器，默认是500
     * @param [options.checkoutEveryNth] {number} 每 N 次事件重新制作一次全量快照
     * @param [options.maskAllInputs] {boolean} 将所有输入内容记录为 *
     * @param [options.maskInputOptions] {} 选择将特定类型的输入框内容记录为 *
     * @param [options.inlineStylesheet] {boolean} 是否将样式表内联
     * @param [options.recordCanvas] {boolean} 是否记录 canvas 内容
     * @param [options.collectFonts] {boolean} 是否记录页面中的字体文件
     * @param [options.instanceOnly] {boolean} 是否仅记录当前Tab内容
     */
    startRecordEvents: function (bizId, description, options) {
      const me = this;
      if (rr._recordEvents) {
        $F.err('当前已有事务在录像中');
        return;
      }

      options = $.extend(options, {
        packSize: 500,
        instanceOnly: false,
        sampling: {
          // 不录制鼠标移动事件
          mousemove: false,
          // 不录制鼠标交互事件
          // mouseInteraction: false,
          // 设置滚动事件的触发频率
          scroll: 100, // 每 100ms 最多触发一次
          // 设置输入事件的录制时机
          // input: 'last' // 连续输入时，只录制最终值
        },
        packFn: rrweb.pack
      })
      if (options.instanceOnly) {
        let proj = $E.getActiveProject();
        $(">.ui-tabs-panel:hidden", proj.tabs).addClass('rr-block')
      }

      $.ajax({
        type: 'POST',
        url: $E.url + '/commons/initEvents',
        data: {
          bizId: bizId
        },
        cache: false,
        async: false,
      }).fail(function (jqXHR, textStatus, errorThrown) {
        $F.err(errorThrown)
      }).done(function (data) {
        if (data.code > 0) {
          rr._recordEvents = {
            bizId: bizId,
            description: description,
            events: []
          }
          options.emit = function (event) {
            // 将 event 存入 events 数组中
            if (rr._recordEvents) {
              rr._recordEvents.events.push(event);
              if (rr._recordEvents.events.length >= options.packSize /* || event.type == 4 || event.type == 2 */)
                me.saveRecordEvents();
            }
          }
          rr._recordEventsStopFn = rrweb.record(options);
          if (options.callback)
            options.callback();

          $F.log('H5Utils - startRecordEvents', 'Initialized rr with bizId:' + bizId + ', description:' + description);
        } else {
          $F.err('H5Utils - startRecordEvents', '业务流水凭据已经存在:' + bizId);
        }
      });
    },

    saveRecordEvents: function (callback) {
      const events = rr._recordEvents.events;
      if (!rr._recordEvents.start) {
        rr._recordEvents.start = rrweb.unpack(events[0]).timestamp;
      }
      rr._recordEvents.end = rrweb.unpack(events[events.length - 1]).timestamp;
      rr._recordEvents.events = [];

      if (!events.length) {
        callback && callback();
        return;
      }
      $.ajax({
        type: 'POST',
        url: $E.url + '/commons/recordEvents',
        data: JSON.stringify({
          bizId: rr._recordEvents.bizId,
          events: events
        }),
        contentType: 'application/json;charset=UTF-8',
        cache: false
      }).fail(function (jqXHR, textStatus, errorThrown) {
        $F.err(errorThrown)
      }).done(function (data) {
        // console.log(data)
        if (data.code > 0) {
          $F.log('H5Utils - saveRecordEvents', 'Saved record events to server, events count ' + events.length)
          callback && callback()
        } else {
          $F.err(data.message)
          rr._recordEventsStopFn();
          rr = {};
          let proj = $E.getActiveProject();
          $(">.ui-tabs-panel", proj.tabs).removeClass('rr-block')
        }
      });
    },

    stopRecordEvents: function (callback) {
      if (!rr._recordEvents) {
        $F.err('当前不在视频录制中')
        return;
      }

      rr._recordEventsStopFn();
      this.saveRecordEvents(function () {
        $.ajax({
          type: 'POST',
          url: $E.url + '/commons/stopEvents',
          data: {
            bizId: rr._recordEvents.bizId,
            description: rr._recordEvents.description,
            start: rr._recordEvents.start,
            end: rr._recordEvents.end,
          },
          cache: false
        }).fail(function (jqXHR, textStatus, errorThrown) {
          $F.err(errorThrown)
        }).done(function (data) {
          if (data.code > 0) {
            callback && callback();
          }
        });
        rr = {};
        let proj = $E.getActiveProject();
        $(">.ui-tabs-panel", proj.tabs).removeClass('rr-block')
      });
    },

    replayEvents: function (bizId) {
      window.open($E.url + '/replay.html#' + bizId);
      // $.ajax({
      // 	type : 'POST',
      // 	url : $E.url + '/commons/replayEvents',
      // 	data : {
      // 		bizId: bizId,
      // 	},
      // 	cache : false
      // }).fail(function(jqXHR, textStatus, errorThrown){
      // 	console.error(errorThrown)
      // }).done(function(data) {
      // 	if (data.code < 0) {
      // 		$F.err(data.message);
      // 		return;
      // 	}
      // 	let events = []
      // 	data.value.forEach(estr => {
      // 		events = events.concat(JSON.parse(estr));
      // 	})
      // 	let dom = $('<div class="rr-player-wrapper">' +
      // 		'<button type="button" class="js_close"><i class="fa fa-times-circle" /></button></div>');
      // 	$('.js_close', dom).click(function() {
      // 		Helper.masker.hide(dom);
      // 	})
      // 	Helper.masker.show(dom);
      // 	new rrwebPlayer({
      // 		target: dom[0],
      // 		data: {
      // 			events: events,
      // 			autoPlay: false,
      // 			control: true,
      // 			height: 600,
      // 			width: 1024
      // 		},
      // 	});
      // });
    },

    //容联SDK5.0.4.1

    // agentAssist: {
    //   token: "",
    //   username: "",
    //   businessId: "",
    //   password: "",
    //   ajaxUrl: "",
    //   ccVue: "",
    //   init: function (url, params) {
    //     var a = this;
    //
    //     a.ajaxUrl = url;
    //     if (this.ccVue) this.ccVue = undefined;
    //     this.ccVue = new Vue({
    //       components: {
    //         agent: agentAssistSdk
    //       }
    //     }).$mount('#agentAssistSdkApp')
    //     $.ajax({
    //       type: "POST",
    //       // url: a.ajaxUrl + "/ai-admin/auth/loginGetToken",
    //       url: a.ajaxUrl + "/ai-admin/public/getToken",
    //       dataType: "json",
    //       data: JSON.stringify(params),
    //       contentType: "application/json",
    //       success: function (data, status, xhr) {
    //         a.token = data.data.token
    //       },
    //       fail: function (xhr) {
    //         alert(JSON.stringify(xhr))
    //       }
    //     })
    //   },
    //   login: function (businessId) {
    //     window.AIHELPER_SDK.login(H5Utils.agentAssist.token, businessId)
    //   },
    //   start: function (callId, sip, businessId, direction = 1, caller = '057156553128', called = '13918921728', username = 'jiang') {
    //     var token = JSON.parse(localStorage.getItem("sdktoken"));
    //     var param = {
    //       token: token,
    //       callId: callId,
    //       sip: sip,
    //       sceneId: businessId,
    //       direction: direction,
    //       caller: caller,
    //       called: called,
    //       username: username
    //     };
    //     console.log("======\u94fe\u63a5websocket", JSON.stringify(param));
    //     if (param.sceneId) window.AIHELPER_SDK.connInitAIhelperSdk(param)
    //   },
    //   close: function () {
    //     window.AIHELPER_SDK.closeAIhelperSdk()
    //   },
    //   logout: function () {
    //     window.AIHELPER_SDK.logout()
    //   }
    // },

    // NGS softphone

    agentAssist: {
      token: '',
      ajaxUrl: '',
      init: function (url, params) {
        const a = this;
        a.ajaxUrl = url;

        $.ajax({
          type : "post",
          url : url + "/uap/user/login",
          data :  JSON.stringify(params),
          contentType : 'application/json;charset=UTF-8',
          success : function(data, status,xhr) {
            var token = xhr.getResponseHeader('authorization');
            window.channel = new MultiChannel(token)
            a.token = token;
          },
          fail : function(xhr) {
            alert(JSON.stringify(xhr));
          }
        })
      },
      start: function (callId, sip, businessId) {
        if(window.channel) {
          channel.connInitOpenSdk(callId, sip, businessId);
          channel.getSdkPower({
            mapNavigation: true,    //思路导航
            knowledgeRecommend: true,   //知识推荐
            infoExtract: true,          //信息提取
            userPortrait: true,      //用户画像
            callId: callId
          })
          eventBusfun.on('assistCallResBackWS', (data) => {
            console.log('cindy', data)
          })

          channel.getMapList(businessId)
          eventBusfun.on('mapList',(data)=>{
            console.log(data)
          })
        }
      },
      sendText: function (position, callId, msg, callback) {
        const a = this;
        $.ajax({
          type: 'post',
          url: '/ias-assist/agent/sdk/sendTextHandler',
          headers: {
            token: a.token
          },
          data: JSON.stringify({
            position: position,
            callId: callId,
            msg: msg,
            authorization: a.token
          }),
          contentType : 'application/json;charset=UTF-8',
          success: function (data, status, xhr) {
            callback && callback(data)
          }
        })
      },
      close: function () {
        if(window.channel) {
          channel.closeWebsocketServe()
        }
      }
    },

    initChatBox: function (url, txt, imageUrl, id, style) {
      require(
        ['js/lib/chat/drag'],
        function () {
          Helper.loadCSS($E.url + '/js/lib/chat/drag.css');
          dragChat(url, txt, imageUrl, id, style);
        }
      )
    },
  };

  var userAgent = navigator.userAgent.toLowerCase();
  if ($E.debug)
    console.log(userAgent);
  if (userAgent.indexOf("chrome") > -1 && userAgent.indexOf("safari") > -1) {
    H5Utils.isChrome = true;
    var str = userAgent.substring(userAgent.indexOf("chrome") + 7),
      version = parseInt(str.split(' ')[0].split('.')[0]);
    if ($E.debug)
      console.log('Chrome version: ' + version);
    if (version <= 53)
      H5Utils.isChromeLegacy = true;
    if (userAgent.indexOf('edge') >= 0) {
      H5Utils.isEdge = true;
      var str2 = userAgent.substring(userAgent.indexOf("edge") + 5),
        version2 = parseInt(str2.split(' ')[0].split('.')[0]);
      if ($E.debug)
        console.log('Edge version: ' + version2);
    }
  }
  if (userAgent.indexOf("firefox") > -1 && userAgent.indexOf("mozilla") > -1) {
    H5Utils.isFirefox = true;
    var str = userAgent.substring(userAgent.indexOf("firefox") + 8),
      version = parseInt(str.split(' ')[0].split('.')[0]);
    if ($E.debug)
      console.log('Firefox version: ' + version);
  }

  var findMinimalWindow = function (id) {
      var wins = $('footer .f_mini_win'),
        x;
      for (var i = 0; i < wins.length; i++) {
        x = $(wins[i]);
        if (x.attr('data-id') == id)
          return x;
      }
      return undefined;
    },

    getStringData = function (data) {
      if ($.type(data) === 'string')
        return data;
      else {
        return JSON.stringify(data);
      }
    },

    removeMsg = function (dialog, msgbox) {
      dialog.removeData('animation');
      if (dialog.is(':visible')) {
        dialog.effect('clip', {}, 500, function () {
          dialog.remove();
        })
      } else {
        if ($E.debug)
          console.log('remove popup message.')
        dialog.remove();
      }
    },

    /**
     * 控制上传
     * 上传dataURL图片
     */
    uploadDataURL = function (content, localpath, uploadUrl, callback, uploadPath) {
      //获取图片的扩展名
      var pos = localpath.lastIndexOf('.'),
        extendName = localpath.substr(pos + 1),
        postData = {
          extendName: extendName,
          uploadPath: uploadPath || '',
          picData: content
        };

      if ($E.debug)
        console.log('Upload dataURL: ' + localpath + ', size:' + content.length + ', upload:' + uploadUrl);

      // 上传的数据除了图片外，还可以包含自己需要传递的参数
      $.ajax({
        type: 'POST',
        url: uploadUrl,
        dataType: "json",
        data: postData,
        cache: false,
        async: true
      }).done(function (data) {
        $F.log('H5Utils - uploadDataURL', 'UploadDataURL Done, download path:' + data.value);
        callback(data);
      }).fail(function (jqXHR, textStatus, errorThrown) {
        $F.err('H5Utils - uploadDataURL', 'UploadDataURL failed: ' + JSON.stringify(errorThrown));
      });
    },
    rr = {};

  window.H5Utils = H5Utils;
})(jQuery)
